/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;

// Original code comments:
// PathGeometryHelper.cs by Charles Petzold, December 2007
//
// This source code obtained from MSDN Magasine Code Foundation (March 2008) and 
// the URL is http://msdn.microsoft.com/en-us/magazine/cc337985.aspx?code=true&level=root,Petzold.Shapes&file=PathGeometryHelper.cs.

namespace Xceed.Silverlight.Utils
{
  internal class PathGeometryHelper
  {
    public PathGeometry FlattenGeometry( Geometry geometry, double tolerance )
    {
      if( geometry == null )
        return new PathGeometry();

      PathGeometry destinationGeometry = new PathGeometry();
      this.FlattenGeometry( destinationGeometry, geometry, tolerance, Matrix.Identity );

      return destinationGeometry;
    }

    private void FlattenGeometry(
      PathGeometry destinationPathGeometry,
      Geometry sourceGeometry,
      double tolerance,
      Matrix previousSourceGeometryTransformMatrix )
    {
      Matrix sourceGeometryTransformMatrix = TransformHelper.GetTransformValue( sourceGeometry.Transform );

      MatrixHelper matxHelper = new MatrixHelper( sourceGeometryTransformMatrix ) * new MatrixHelper( previousSourceGeometryTransformMatrix );

      Matrix matrix = matxHelper.Value;
      PathFigure figure = null;

      if( sourceGeometry is GeometryGroup )
      {
        GeometryGroup group = sourceGeometry as GeometryGroup;

        foreach( Geometry child in group.Children )
        {
          this.FlattenGeometry( destinationPathGeometry, child, tolerance, matrix );
        }

        return;
      }

      if( sourceGeometry is LineGeometry )
      {
        figure = this.CreateFigureForLineGeometry( sourceGeometry as LineGeometry, matrix );
      }
      else if( sourceGeometry is RectangleGeometry )
      {
        figure = this.CreateFigureForRectangleGeometry( sourceGeometry as RectangleGeometry, matrix, tolerance );
      }
      else if( sourceGeometry is EllipseGeometry )
      {
        figure = this.CreateFigureForEllipseGeometry( sourceGeometry as EllipseGeometry, matrix, tolerance );
      }
      else if( sourceGeometry is PathGeometry )
      {
        PathGeometry pathGeometry = sourceGeometry as PathGeometry;
        destinationPathGeometry.FillRule = pathGeometry.FillRule;
        figure = this.CreateFigureForPathGeometry( pathGeometry, matrix, tolerance );
      }

      destinationPathGeometry.Figures.Add( figure );
    }

    private PathFigure CreateFigureForPathGeometry(
      PathGeometry pathGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( pathGeometry == null )
        throw new ArgumentNullException( "pathGeometry" );

      PathFigure destinationFigure = new PathFigure();
      int figureCount = pathGeometry.Figures.Count;

      for( int i = 0; i < figureCount; i++ )
      {
        PathFigure sourceFigure = pathGeometry.Figures[ i ];

        destinationFigure.IsFilled = sourceFigure.IsFilled;
        destinationFigure.IsClosed = sourceFigure.IsClosed;
        destinationFigure.StartPoint = matrix.Transform( sourceFigure.StartPoint );
        Point lastPoint = destinationFigure.StartPoint;

        int segmentCount = sourceFigure.Segments.Count;

        for( int y = 0; y < segmentCount; y++ )
        {
          PathSegment sourceSegment = sourceFigure.Segments[ y ];

          PolyLineSegment destinationSegment = new PolyLineSegment();

          if( sourceSegment is LineSegment )
          {
            LineSegment lineSegment = sourceSegment as LineSegment;
            lastPoint = matrix.Transform( lineSegment.Point );
            destinationSegment.Points.Add( lastPoint );
          }
          else if( sourceSegment is PolyLineSegment )
          {
            PolyLineSegment polyLineSegment = sourceSegment as PolyLineSegment;

            foreach( Point point in polyLineSegment.Points )
            {
              lastPoint = matrix.Transform( point );
              destinationSegment.Points.Add( lastPoint );
            }
          }
          else if( sourceSegment is BezierSegment )
          {
            BezierSegment bezSeg = sourceSegment as BezierSegment;

            Point pt0 = lastPoint;
            Point pt1 = matrix.Transform( bezSeg.Point1 );
            Point pt2 = matrix.Transform( bezSeg.Point2 );
            Point pt3 = matrix.Transform( bezSeg.Point3 );

            this.FlattenCubicBezier( destinationSegment.Points, pt0, pt1, pt2, pt3, tolerance );

            lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
          }
          else if( sourceSegment is PolyBezierSegment )
          {
            PolyBezierSegment polyBezSeg = sourceSegment as PolyBezierSegment;

            PointCollection polyBezierSegmentPoints = polyBezSeg.Points;
            int pointCount = polyBezierSegmentPoints.Count;

            for( int bez = 0; bez < pointCount; bez += 3 )
            {
              if( bez + 2 > pointCount - 1 )
                break;

              Point pt0 = lastPoint;
              Point pt1 = matrix.Transform( polyBezierSegmentPoints[ bez ] );
              Point pt2 = matrix.Transform( polyBezierSegmentPoints[ bez + 1 ] );
              Point pt3 = matrix.Transform( polyBezierSegmentPoints[ bez + 2 ] );

              this.FlattenCubicBezier( destinationSegment.Points, pt0, pt1, pt2, pt3, tolerance );

              lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
            }
          }
          else if( sourceSegment is QuadraticBezierSegment )
          {
            QuadraticBezierSegment quadBezSeg = sourceSegment as QuadraticBezierSegment;
            Point pt0 = lastPoint;
            Point pt1 = matrix.Transform( quadBezSeg.Point1 );
            Point pt2 = matrix.Transform( quadBezSeg.Point2 );

            this.FlattenQuadraticBezier( destinationSegment.Points, pt0, pt1, pt2, tolerance );

            lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
          }
          else if( sourceSegment is PolyQuadraticBezierSegment )
          {
            PolyQuadraticBezierSegment polyQuadBezSeg = sourceSegment as PolyQuadraticBezierSegment;

            PointCollection points = polyQuadBezSeg.Points;
            int pointCount = points.Count;

            for( int bez = 0; bez < pointCount; bez += 2 )
            {
              if( bez + 1 > pointCount - 1 )
                break;

              Point pt0 = lastPoint;
              Point pt1 = matrix.Transform( points[ bez ] );
              Point pt2 = matrix.Transform( points[ bez + 1 ] );

              this.FlattenQuadraticBezier( destinationSegment.Points, pt0, pt1, pt2, tolerance );

              lastPoint = destinationSegment.Points[ destinationSegment.Points.Count - 1 ];
            }
          }
          else if( sourceSegment is ArcSegment )
          {
            ArcSegment arcSeg = sourceSegment as ArcSegment;

            PointCollection destinationPoints = destinationSegment.Points;

            this.FlattenArc( destinationPoints,
              lastPoint,
              matrix.Transform( arcSeg.Point ),
              matrix.Transform( new Point( arcSeg.Size.Width, 0 ) ).X,
              matrix.Transform( new Point( 0, arcSeg.Size.Height ) ).Y,
              arcSeg.RotationAngle,
              arcSeg.IsLargeArc,
              arcSeg.SweepDirection == SweepDirection.Counterclockwise,
              tolerance );

            if( destinationPoints.Count > 0 )
              lastPoint = destinationPoints[ destinationPoints.Count - 1 ];
          }

          destinationFigure.Segments.Add( destinationSegment );
        }
      }

      return destinationFigure;
    }

    private PathFigure CreateFigureForEllipseGeometry(
      EllipseGeometry ellipseGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( ellipseGeometry == null )
        throw new ArgumentNullException( "ellipseGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      int max = ( int )( 4 * ( ellipseGeometry.RadiusX + ellipseGeometry.RadiusY ) / tolerance );

      for( int i = 0; i < max; i++ )
      {
        double x = ellipseGeometry.Center.X + ellipseGeometry.RadiusX * Math.Sin( i * 2 * Math.PI / max );
        double y = ellipseGeometry.Center.Y - ellipseGeometry.RadiusY * Math.Cos( i * 2 * Math.PI / max );
        Point pt = matrix.Transform( new Point( x, y ) );

        if( i == 0 )
        {
          figure.StartPoint = pt;
        }
        else
        {
          segment.Points.Add( pt );
        }
      }

      figure.IsClosed = true;
      figure.Segments.Add( segment );
      return figure;
    }

    private PathFigure CreateFigureForRectangleGeometry(
      RectangleGeometry rectangleGeometry,
      Matrix matrix,
      double tolerance )
    {
      if( rectangleGeometry == null )
        throw new ArgumentNullException( "rectangleGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      if( rectangleGeometry.RadiusX == 0 || rectangleGeometry.RadiusY == 0 )
      {
        figure.StartPoint = matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopLeft );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopRight ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).BottomRight ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).BottomLeft ) );
        segment.Points.Add( matrix.Transform( new RectHelper( rectangleGeometry.Rect ).TopLeft ) );
      }
      else
      {
        double radiusX = Math.Min( rectangleGeometry.Rect.Width / 2, rectangleGeometry.RadiusX );
        double radiusY = Math.Min( rectangleGeometry.Rect.Height / 2, rectangleGeometry.RadiusY );
        Rect rect = rectangleGeometry.Rect;

        figure.StartPoint = matrix.Transform( new Point( rect.Left + radiusX, rect.Top ) );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Right - radiusX, rect.Top ),
          new Point( rect.Right, rect.Top + radiusY ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Right, rect.Bottom - radiusY ),
          new Point( rect.Right - radiusX, rect.Bottom ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Left + radiusX, rect.Bottom ),
          new Point( rect.Left, rect.Bottom - radiusY ),
          radiusX, radiusY, tolerance );

        this.AddCorner( segment,
          matrix,
          new Point( rect.Left, rect.Top + radiusY ),
          new Point( rect.Left + radiusX, rect.Top ),
          radiusX, radiusY, tolerance );
      }

      figure.IsClosed = true;
      figure.Segments.Add( segment );
      return figure;
    }

    private PathFigure CreateFigureForLineGeometry(
      LineGeometry lineGeometry,
      Matrix matrix )
    {
      if( lineGeometry == null )
        throw new ArgumentNullException( "lineGeometry" );

      PathFigure figure = new PathFigure();
      PolyLineSegment segment = new PolyLineSegment();

      figure.StartPoint = matrix.Transform( lineGeometry.StartPoint );
      segment.Points.Add( matrix.Transform( lineGeometry.EndPoint ) );

      figure.Segments.Add( segment );
      return figure;
    }

    private void FlattenCubicBezier(
      PointCollection points,
      Point ptStart,
      Point ptCtrl1,
      Point ptCtrl2,
      Point ptEnd,
      double tolerance )
    {
      int max = ( int )( ( ( new PointHelper( ptCtrl1 ) - new PointHelper( ptStart ) ).Length +
                           ( new PointHelper( ptCtrl2 ) - new PointHelper( ptCtrl1 ) ).Length +
                           ( new PointHelper( ptEnd ) - new PointHelper( ptCtrl2 ) ).Length ) / tolerance );

      for( int i = 0; i <= max; i++ )
      {
        double t = ( double )i / max;

        double x = ( 1 - t ) * ( 1 - t ) * ( 1 - t ) * ptStart.X +
                   3 * t * ( 1 - t ) * ( 1 - t ) * ptCtrl1.X +
                   3 * t * t * ( 1 - t ) * ptCtrl2.X +
                   t * t * t * ptEnd.X;

        double y = ( 1 - t ) * ( 1 - t ) * ( 1 - t ) * ptStart.Y +
                   3 * t * ( 1 - t ) * ( 1 - t ) * ptCtrl1.Y +
                   3 * t * t * ( 1 - t ) * ptCtrl2.Y +
                   t * t * t * ptEnd.Y;

        points.Add( new Point( x, y ) );
      }
    }

    private void FlattenQuadraticBezier(
      PointCollection points,
      Point ptStart,
      Point ptCtrl,
      Point ptEnd,
      double tolerance )
    {
      int max = ( int )( ( ( new PointHelper( ptCtrl ) - new PointHelper( ptStart ) ).Length +
                          ( new PointHelper( ptEnd ) - new PointHelper( ptCtrl ) ).Length ) / tolerance );

      for( int i = 0; i <= max; i++ )
      {
        double t = ( double )i / max;

        double x = ( 1 - t ) * ( 1 - t ) * ptStart.X +
                   2 * t * ( 1 - t ) * ptCtrl.X +
                   t * t * ptEnd.X;

        double y = ( 1 - t ) * ( 1 - t ) * ptStart.Y +
                   2 * t * ( 1 - t ) * ptCtrl.Y +
                   t * t * ptEnd.Y;

        points.Add( new Point( x, y ) );
      }
    }

    private void FlattenArc(
      IList<Point> points,
      Point pt1,
      Point pt2,
      double radiusX,
      double radiusY,
      double angleRotation,
      bool isLargeArc,
      bool isCounterclockwise,
      double tolerance )
    {
      // Get info about chord that connects both points
      Point midPoint = new Point( ( pt1.X + pt2.X ) / 2, ( pt1.Y + pt2.Y ) / 2 );
      Vector vect = new PointHelper( pt2 ) - new PointHelper( pt1 );
      double halfChord = vect.Length / 2;

      // Get vector from chord to center
      Vector vectRotated;

      // (comparing two Booleans here!)
      if( isLargeArc == isCounterclockwise )
      {
        vectRotated = new Vector( -vect.Y, vect.X );
      }
      else
      {
        vectRotated = new Vector( vect.Y, -vect.X );
      }

      vectRotated.Normalize();

      // Distance from chord to center 
      double centerDistance = Math.Sqrt( radiusY * radiusY - halfChord * halfChord );

      // Calculate two center points
      Point center = midPoint + centerDistance * vectRotated;

      // Get angles from center to the two points
      double angle1 = Math.Atan2( pt1.Y - center.Y, pt1.X - center.X );
      double angle2 = Math.Atan2( pt2.Y - center.Y, pt2.X - center.X );

      // (another comparison of two Booleans!)
      if( isLargeArc == ( Math.Abs( angle2 - angle1 ) < Math.PI ) )
      {
        if( angle1 < angle2 )
        {
          angle1 += 2 * Math.PI;
        }
        else
        {
          angle2 += 2 * Math.PI;
        }
      }

      // Calculate number of points for polyline approximation
      int max = ( int )( ( 4 * ( radiusX + radiusY ) * Math.Abs( angle2 - angle1 ) / ( 2 * Math.PI ) ) / tolerance );

      // Loop through the points
      for( int i = 0; i <= max; i++ )
      {
        double angle = ( ( max - i ) * angle1 + i * angle2 ) / max;
        double x = center.X + radiusY * Math.Cos( angle );
        double y = center.Y + radiusY * Math.Sin( angle );

        // Transform the point back
        points.Add( new Point( x, y ) );
      }
    }

    // Helper routine for RectangleGeometry
    private void AddCorner(
      PolyLineSegment segDst,
      Matrix matx,
      Point pt1,
      Point pt2,
      double radiusX,
      double radiusY,
      double tolerance )
    {
      List<Point> points = new List<Point>();

      this.FlattenArc( points,
        pt1,
        pt2,
        radiusX,
        radiusY,
        0,
        false,
        false,
        tolerance );

      int pointsCount = points.Count;

      for( int i = 0; i < pointsCount; i++ )
        segDst.Points.Add( matx.Transform( points[ i ] ) );
    }
  }
}
